summaryrefslogtreecommitdiff
path: root/app/[lng]/evcp
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]/evcp')
-rw-r--r--app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx2
-rw-r--r--app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx11
-rw-r--r--app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx112
-rw-r--r--app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx67
4 files changed, 161 insertions, 31 deletions
diff --git a/app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx
index b675aed1..eb5e62d0 100644
--- a/app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx
@@ -61,7 +61,7 @@ export default async function SettingsLayout({
{/* 4) 입찰 정보가 있으면 번호 + 제목 + "상세 정보" 표기 */}
<h2 className="text-2xl font-bold tracking-tight">
{bidding
- ? `${bidding.biddingNumber ?? ""} - ${bidding.title}`
+ ? `입찰 No. ${bidding.biddingNumber ?? ""} - ${bidding.title}`
: "Loading Bidding..."}
</h2>
</div>
diff --git a/app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx b/app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx
index e2c22b22..64d6d740 100644
--- a/app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx
+++ b/app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx
@@ -1,7 +1,8 @@
import { Suspense } from 'react'
import { notFound } from 'next/navigation'
import { getBiddingDetailData } from '@/lib/bidding/detail/service'
-import { BiddingDetailContent } from '@/lib/bidding/detail/table/bidding-detail-content'
+import { getBiddingCompanies } from '@/lib/bidding/pre-quote/service'
+import { BiddingPreQuoteContent } from '@/lib/bidding/pre-quote/table/bidding-pre-quote-content'
// 메타데이터 생성
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
@@ -38,13 +39,17 @@ export default async function Page({ params }: PageProps) {
notFound()
}
+ // 사전견적용 입찰 업체들 조회
+ const biddingCompaniesResult = await getBiddingCompanies(parsedId)
+ const biddingCompanies = biddingCompaniesResult.success ? biddingCompaniesResult.data : []
+
return (
<Suspense fallback={<div className="p-8">로딩 중...</div>}>
- <BiddingDetailContent
+ <BiddingPreQuoteContent
bidding={detailData.bidding}
quotationDetails={detailData.quotationDetails}
quotationVendors={detailData.quotationVendors}
- biddingCompanies={detailData.biddingCompanies}
+ biddingCompanies={biddingCompanies}
prItems={detailData.prItems}
/>
</Suspense>
diff --git a/app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx
index 1b058801..999bfe8b 100644
--- a/app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx
@@ -4,9 +4,12 @@ import { Separator } from "@/components/ui/separator"
import { SidebarNav } from "@/components/layout/sidebar-nav"
import { formatDate } from "@/lib/utils"
import { Button } from "@/components/ui/button"
-import { ArrowLeft } from "lucide-react"
+import { Badge } from "@/components/ui/badge"
+import { ArrowLeft, Clock, AlertTriangle, CheckCircle, XCircle, AlertCircle } from "lucide-react"
import { RfqsLastView } from "@/db/schema"
import { findRfqLastById } from "@/lib/rfq-last/service"
+import { differenceInDays } from "date-fns"
+import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
export const metadata: Metadata = {
title: "견적 목록 상세",
@@ -23,30 +26,92 @@ export default async function RfqLayout({
// 1) URL 파라미터에서 id 추출, Number로 변환
const resolvedParams = await params
const lng = resolvedParams.lng
- const id = resolvedParams.id
+ const rfqId = parseInt(resolvedParams.id, 10);
+
+ if (!rfqId || isNaN(rfqId) || rfqId <= 0) {
+ return (
+ <div className="p-4">
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>오류</AlertTitle>
+ <AlertDescription>
+ 유효하지 않은 RFQ입니다.
+ </AlertDescription>
+ </Alert>
+ </div>
+ );
+ }
+
- const idAsNumber = Number(id)
// 2) DB에서 해당 협력업체 정보 조회
- const rfq: RfqsLastView | null = await findRfqLastById(idAsNumber)
+ const rfq: RfqsLastView | null = await findRfqLastById(rfqId)
// 3) 사이드바 메뉴
const sidebarNavItems = [
{
title: "견적 문서관리",
- href: `/${lng}/evcp/rfq-last/${id}`,
+ href: `/${lng}/evcp/rfq-last/${rfqId}`,
},
{
title: "RFQ 발송",
- href: `/${lng}/evcp/rfq-last/${id}/vendor`,
+ href: `/${lng}/evcp/rfq-last/${rfqId}/vendor`,
},
]
+ // Due Date 상태 계산 함수
+ const getDueDateStatus = (dueDate: Date | string | null) => {
+ if (!dueDate) return null;
+
+ const now = new Date();
+ const due = new Date(dueDate);
+ const daysLeft = differenceInDays(due, now);
+
+ if (daysLeft < 0) {
+ return {
+ icon: <XCircle className="h-4 w-4" />,
+ text: `${Math.abs(daysLeft)}일 지남`,
+ className: "text-red-600",
+ bgClassName: "bg-red-50"
+ };
+ } else if (daysLeft === 0) {
+ return {
+ icon: <AlertTriangle className="h-4 w-4" />,
+ text: "오늘 마감",
+ className: "text-orange-600",
+ bgClassName: "bg-orange-50"
+ };
+ } else if (daysLeft <= 3) {
+ return {
+ icon: <AlertCircle className="h-4 w-4" />,
+ text: `${daysLeft}일 남음`,
+ className: "text-amber-600",
+ bgClassName: "bg-amber-50"
+ };
+ } else if (daysLeft <= 7) {
+ return {
+ icon: <Clock className="h-4 w-4" />,
+ text: `${daysLeft}일 남음`,
+ className: "text-blue-600",
+ bgClassName: "bg-blue-50"
+ };
+ } else {
+ return {
+ icon: <CheckCircle className="h-4 w-4" />,
+ text: `${daysLeft}일 남음`,
+ className: "text-green-600",
+ bgClassName: "bg-green-50"
+ };
+ }
+ };
+
+ const dueDateStatus = rfq?.dueDate ? getDueDateStatus(rfq.dueDate) : null;
+
return (
<>
<div className="container py-6">
<section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
<div className="hidden space-y-6 p-10 pb-16 md:block">
- <div className="flex items-center justify-end mb-4">
+ <div className="flex items-center justify-end mb-4">
<Link href={`/${lng}/evcp/rfq-last`} passHref>
<Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto">
<ArrowLeft className="mr-1 h-4 w-4" />
@@ -55,25 +120,38 @@ export default async function RfqLayout({
</Link>
</div>
<div className="space-y-0.5">
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */}
+ {/* 제목 로직 수정: rfqTitle 있으면 사용, 없으면 rfqCode만 표시 */}
<h2 className="text-2xl font-bold tracking-tight">
{rfq
- ? `${rfq.rfqCode ?? ""} | ${rfq.packageNo ?? ""} | ${rfq.packageName ?? ""}`
+ ? rfq.rfqTitle
+ ? `견적 상세 관리 ${rfq.rfqCode ?? ""} | ${rfq.rfqTitle}`
+ : `견적 상세 관리 ${rfq.rfqCode ?? ""}`
: "Loading RFQ..."}
</h2>
-
- <p className="text-muted-foreground">
- RFQ 관리하는 화면입니다.
- </p>
- <h3>Due Date:{rfq && rfq?.dueDate && <strong>{formatDate(rfq?.dueDate, "KR")}</strong>}</h3>
+
+ {/* <p className="text-muted-foreground">
+ RFQ 관리하는 화면입니다.
+ </p> */}
+
+ {/* Due Date 표시 개선 */}
+ {rfq?.dueDate && dueDateStatus && (
+ <div className="flex items-center gap-3 pt-2">
+ <span className="text-sm font-medium text-muted-foreground">Due Date:</span>
+ <strong className="text-sm">{formatDate(rfq.dueDate, "KR")}</strong>
+ <div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full ${dueDateStatus.bgClassName} ${dueDateStatus.className}`}>
+ {dueDateStatus.icon}
+ <span className="text-xs font-medium">{dueDateStatus.text}</span>
+ </div>
+ </div>
+ )}
</div>
<Separator className="my-6" />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
- <aside className="lg:w-64 flex-shrink-0">
- <SidebarNav items={sidebarNavItems} />
+ <aside className="lg:w-64 flex-shrink-0">
+ <SidebarNav items={sidebarNavItems} />
</aside>
<div className="lg:w-[calc(100%-16rem)] overflow-auto">{children}</div>
- </div>
+ </div>
</div>
</section>
</div>
diff --git a/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx b/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx
index 6819e122..1ccb7559 100644
--- a/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx
+++ b/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx
@@ -3,6 +3,9 @@ import { type SearchParams } from "@/types/table"
import { getValidFilters } from "@/lib/data-table"
import { searchParamsRfqAttachmentsCache } from "@/lib/b-rfq/validations"
import { getRfqLastAttachments } from "@/lib/rfq-last/service"
+import { RfqAttachmentsTable } from "@/lib/rfq-last/attachment/rfq-attachments-table"
+import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
+import { AlertCircle } from "lucide-react"
interface IndexPageProps {
// Next.js 13 App Router에서 기본으로 주어지는 객체들
@@ -16,21 +19,61 @@ interface IndexPageProps {
export default async function RfqPage(props: IndexPageProps) {
const resolvedParams = await props.params
const lng = resolvedParams.lng
- const id = resolvedParams.id
+ const rfqId = parseInt(resolvedParams.id, 10);
- const idAsNumber = Number(id)
+ if (!rfqId || isNaN(rfqId) || rfqId <= 0) {
+ return (
+ <div className="p-4">
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>오류</AlertTitle>
+ <AlertDescription>
+ 유효하지 않은 RFQ입니다.
+ </AlertDescription>
+ </Alert>
+ </div>
+ );
+ }
// 2) SearchParams 파싱 (Zod)
// - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼
- const searchParams = await props.searchParams
- const search = searchParamsRfqAttachmentsCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
+ const searchParams = await props.searchParams;
+ const activeTab = searchParams.tab || '설계';
+
+ // 활성 탭에 따라 다른 파라미터 파싱
+ const designSearch = activeTab === '설계'
+ ? searchParamsRfqAttachmentsCache.parse({
+ ...searchParams,
+ // design_ prefix가 붙은 파라미터들 추출
+ page: searchParams.design_page,
+ perPage: searchParams.design_perPage,
+ sort: searchParams.design_sort,
+ filters: searchParams.design_filters,
+ })
+ : { page: 1, perPage: 10, sort: [], filters: [] };
+
+ const purchaseSearch = activeTab === '구매'
+ ? searchParamsRfqAttachmentsCache.parse({
+ ...searchParams,
+ // purchase_ prefix가 붙은 파라미터들 추출
+ page: searchParams.purchase_page,
+ perPage: searchParams.purchase_perPage,
+ sort: searchParams.purchase_sort,
+ filters: searchParams.purchase_filters,
+ })
+ : { page: 1, perPage: 10, sort: [], filters: [] };
+
+ // 활성 탭의 데이터만 실제로 가져오기
+ const [designData, purchaseData] = await Promise.all([
+ activeTab === '설계'
+ ? getRfqLastAttachments({ ...designSearch }, rfqId, "설계")
+ : { data: [], pageCount: 0 },
+ activeTab === '구매'
+ ? getRfqLastAttachments({ ...purchaseSearch }, rfqId, "구매")
+ : { data: [], pageCount: 0 }
+ ]);
- const promises = getRfqLastAttachments({
- ...search,
- filters: validFilters,
- }, idAsNumber)
// 4) 렌더링
return (
@@ -45,7 +88,11 @@ export default async function RfqPage(props: IndexPageProps) {
</div>
<Separator />
<div>
- {/* <RfqAttachmentsTable promises={promises} rfqId={idAsNumber} /> */}
+ <RfqAttachmentsTable
+ rfqId={rfqId}
+ initialDesignData={designData}
+ initialPurchaseData={purchaseData}
+ />
</div>
</div>
)